Rendering using our own shader

Now that we can enjoy our sphere from all angles, it is time to customize the way it is rendered. This means writing our own shader to alter the rendering directly on GPU.

As we are using a DirectX renderer, we will write it in HLSL. If you are unfamiliar with this language, it is advised to check some documentation about it to fully understand what we will be doing here. For instance, you can find some good knowledge here or there.

Creating a Program

Within nkGraphics, the class symbolizing a shader program is called Program. Let's start manipulating it, by first including :

#include <NilkinsGraphics/Programs/Program.h> #include <NilkinsGraphics/Programs/ProgramManager.h> #include <NilkinsGraphics/Programs/ProgramSourcesHolder.h>

Having those includes will allow to alter the code to use a Program. Let's put this code right after we create the entity and populate it with the mesh :

nkGraphics::Program* program = nkGraphics::ProgramManager::getInstance()->createOrRetrieve("program") ;

A Program is a resource like any other, and as such we use its dedicated manager to get one. Our local program will be named "program"... How original.

nkGraphics::ProgramSourcesHolder sources ; sources.setVertexMemory ( R"eos( cbuffer PassBuffer : register(b0) { matrix view ; matrix proj ; } struct VertexInput { float4 position : POSITION ; matrix world : WORLDMAT ; } ; struct PixelInput { float4 position : SV_POSITION ; } ; PixelInput main (VertexInput input) { PixelInput result ; matrix mvp = mul(input.world, mul(view, proj)) ; result.position = mul(input.position, mvp) ; return result ; } )eos" ) ; sources.setPixelMemory ( R"eos( struct PixelInput { float4 position : SV_POSITION ; } ; float4 main (PixelInput input) : SV_TARGET { return float4(0.8, 0.4, 0.2, 1.0) ; } )eos" ) ; program->setFromMemory(sources) ; program->load() ;

The most important part of a Program are the sources. They go into a ProgramSourcesHolder structure which can be submitted to the Program for compilation.

As we do a simple shader supposed to paint a mesh, we will need the vertex and pixel stages.
We won't go over the details of the HLSL code in this tutorial, as its only aim is to transform the mesh onto the screen and apply a constant colour.
However, an important point to notice is the fact that the entry point methods should be called main. Those are used by the program compilation within the component.

Back to C++, we specify the program should load from the memory we just populated, and request a load operation, as usual with resources in the component.

Now, notice the vertex stage input :

We need a way to provide this input to the program we just created. This is where we introduce the Shader class.

Controlling the input through a Shader

A Shader represents an association between a Program and an input. It is possible to have one Program linked to many Shaders, each with different input to feed it.

Now that we know the next step we need, let's see the required includes :

#include <NilkinsGraphics/Shaders/Memory/ConstantBuffer.h> #include <NilkinsGraphics/Shaders/Memory/ShaderInstanceMemorySlot.h> #include <NilkinsGraphics/Shaders/Memory/ShaderPassMemorySlot.h> #include <NilkinsGraphics/Shaders/Shader.h> #include <NilkinsGraphics/Shaders/ShaderManager.h>

And use them right away :

nkGraphics::Shader* shader = nkGraphics::ShaderManager::getInstance()->createOrRetrieve("shader") ;

Our shader is now created. Let's see what we can do with it :

shader->setAttachedShaderProgram(program) ; nkGraphics::ConstantBuffer* cBuffer = shader->addConstantBuffer(0) ; nkGraphics::ShaderPassMemorySlot* slot = cBuffer->addPassMemorySlot() ; slot->setAsViewMatrix() ; slot = cBuffer->addPassMemorySlot() ; slot->setAsProjectionMatrix() ; nkGraphics::ShaderInstanceMemorySlot* instanceSlot = shader->addInstanceMemorySlot() ; instanceSlot->setAsWorldMatrix() ; shader->load() ;

We start by attaching its program right away; the one we just created. Next, we register a ConstantBuffer inside, which is precisely what the Program needs.

The ConstantBuffer is formed by ShaderPassMemorySlots. Basically, a pass slot is fed once per pass, right before rendering it. Slots offer many capabilities, and amongst them is feeding a constant buffer with the view and projection matrices.
This relates directly to the constant buffer given within the program, so the order of declaration is important. First comes the view matrix, then comes the projection matrix, like we declared them in HLSL.

Next is the creation of a ShaderInstanceMemorySlot. This type of slot is specific to the rendering of a render queue. Basically, it is triggered once per instance of mesh that needs to be rendered.

Within the HLSL, we have the vertex position, and the world matrix. The vertex position is given by the mesh itself. However, the world matrix depends on the Entity, and the Node it is attached to, changing its position, orientation, or scale within the virtual world.
This specific data is given through the slot meant for instances. By specifying we want it to feed the world matrix, we ensure it will do so for each instance in our rendering.

Final step is to load the shader, like we always do with resources we use. Now the shader is ready to be used during rendering, and to do so, we need to alter the Entity once more :

ent->setShader(shader) ;

Let's see right away what we will get when launching the program !

Red sphere
The sphere turned red !

This covers the way to create and use a shader within the component. For now, it is pretty simple, but in the next steps we will see how to spice it a bit.